package org.tigr.htc.cmd;

/*
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;

import org.apache.log4j.Logger;
import org.exolab.castor.jdo.TimeStampable;
import org.tigr.antware.shared.command.events.CommandInterrupt;
import org.tigr.antware.shared.command.events.FailureEvent;
import org.tigr.antware.shared.command.events.FinishEvent;
import org.tigr.antware.shared.command.events.InterruptEvent;
import org.tigr.antware.shared.command.events.StartEvent;
import org.tigr.antware.shared.command.events.SubmitEvent;
import org.tigr.htc.persist.IPersist;
import org.tigr.htc.persist.PersistFactory;

/**
 * The <code>Command</code> class is a value object that is used to pass
 * all the information about a Command to the grid. Command can be
 * saved to XML or a database.  Command is a generic object and its
 * data fields maybe used differently based on the command type.
 * An example might be if the Command type is for Condor versus
 * SGE.
 */
public class Command extends CommandImp implements Serializable, TimeStampable {

	private static final long serialVersionUID = 1L;

	/** The field <code>log</code>           */
    static private Logger logger = Logger.getLogger(Command.class);
    
    /** The field <code>tasks</code>           */
    public TaskGroup tasks;
    
    // constant defaults
    /** The field <code>DEF_RETRYCOUNT</code>           */
    final static int DEF_RETRYCOUNT = 1;
    
    /** The field <code>DEF_NOW</code>           */
    final static int DEF_NOW = 0;
    
    /** The field <code>DEF_INITIALDIR</code>           */
    final static int SUCCESS = 0;
    
    // 
    // member classes
    // 
    
    /** The field <code>config</code>           */
    protected CommandConfig config;
    
    /** The field <code>status</code>           */
    protected CommandStatus status;
    
    /** The field <code>params</code>           */
    protected ArrayList params;
    
    // 
    // command variables
    // 
    
    /** The field <code>initialDir</code>           */
    protected String initialDir;
    
    /** The field <code>output</code>           */
    protected String output;
    
    /** The field <code>input</code>           */
    protected String input;
    
    /** The field <code>error</code>           */
    protected String error;
    
    /** The field <code>command</code>           */
    protected String command;
    
    /** The field <code>retryCount</code>           */
    protected int retryCount;
    
    /** The field <code>times</code>           */
    protected int times;
    
    /** The field <code>gridJobID</code>           */
    protected String gridJobID;
    
    /** The field <code>maxWorkers</code>           */
    protected int maxWorkers;
    
    /** The field <code>numTasks</code>           */
    protected int numTasks;
    
    /** The field <code>queue</code>           */
    protected String queue;
    
    /**
     * The variable <code>monitorHost</code> holds the name of the htc service host
     * monitoring this command
     */
    protected String monitorHost;

    /** The field <code>requestTime</code> stores the time a request was received */
    //protected Date requestTime;
    
    protected long requestTime = -1;
    
    /**
     * The field <code>sendEvents</code> stores the flag to indicate if the client is awaiting
     * events from the server. This flag is particularly important for re-monitoring where the
     * flag state is checked in the database to decide if a listener should be added.
     */
    protected boolean sendEvents;
    
    public boolean persisted = false;
    static protected IPersist cmdPersist = PersistFactory.getIPersist();
    
    /** The field <code>timeStamp</code>           */
    private long timeStamp;
    
    public static long createCmdID() {
        return cmdPersist.getNewID();
    }
    
    /**
     * Creates a new <code>Command</code> instance with default value.
     */
    public Command() {
        
        status = new CommandStatus();
        
        status.setCommand(this);
        
        params = new ArrayList();
        config = new CommandConfig();
        
        config.setCommand(this);
        
        // by default a Command has one task
        tasks = new TaskGroup();
        retryCount = DEF_RETRYCOUNT;
        times = 0;
        numTasks = 0;
        maxWorkers = 0;
        type = "mw";
    }
    
    /**
     * <code>setID</code> set command ID.
     *
     * @param id an <code>long</code> command id
     */
    @Override
    public void setID(long id) {        
        this.id = id;
        tasks.setCmdId(id);
    }
    
    
    /**
     * <code>isValid</code> does the command have the bare minimum values set.
     *
     * @return a <code>boolean</code> value
     */
    public boolean isValid() {
        if ((group == null) || (username == null) || (command == null) || (type == null)) {
            return false;
        }
        
        return true;
    }
    
    /**
     * @return
     */
    public boolean isPersisted() {
        return persisted;
    }
    
    /**
     * @param pPersisted
     */
    public void setPersisted(boolean persisted) {
        this.persisted = persisted;
    }
    
    /* (non-Javadoc)
     * @see org.tigr.htc.cmd.ICommand#createCommand()
     */
    public synchronized void createCommand() {
        
        if (this.getID() == 0) {
            logger.warn("Command had an id of zero !!! Resetting it to " + this.getID());
            this.setID(cmdPersist.getNewID());
        }
        
        tasks.setPersisted(true);
        
        if (persisted) {
            logger.warn("Command " + this.getID() + " is already persisted");
            // TODO  throw an exception
            return;
        }
        
        logger.info("START CREATE new command with " + id);
        
        if (logger.isDebugEnabled()) {
            logger.debug("Config: " + config);
        }
        
        // Set the request time
        requestTime = new Date().getTime();
        
        if (logger.isDebugEnabled()) {
        	logger.debug("Request time: " + new Date(requestTime).toString());
        }
        
        // Persist this command to the data store
        cmdPersist.create(this);
        persisted = true;
        logger.info("DONE CREATE new Command with " + id);
    }
    
    public synchronized void updateCommand() {
        if (persisted) {
            if (cmdPersist == null) {
                logger.fatal("Command persist object NULL!!!");
                return;
            }
            
            logger.info("START UPDATE command " + id);
            cmdPersist.update(this);
            logger.info("DONE UPDATE command " + id);
        } 
        else {
            logger.debug("Attempt to update Command when it was not persisted.");
        }
    }
    
    //////////////////
    // Setters/Getters
    /////////////////
    
    /**
     * Get the Times value.
     *
     * @return the Times value.
     */
    public int getTimes() {
        return times;
    }
    
    /**
     * Set the Times value.
     *
     * @param times
     *            The new Times value.
     */
    public void setTimes(int times) {
        this.times = times;
    }
    
    /**
     * <code>getInitialDir</code> get the path of the initial working directory for the
     * request.
     *
     * @return a <code>String</code> value
     */
    public String getInitialDir() {
        return initialDir;
    }
    
    /**
     * <code>setInitialDir</code> set the current working path of the Command when it
     * is executed. If this initial directory is set everything else (such as output and
     * error) can be relative from it EXCEPT for the command.
     *
     * @param initialDir a <code>String</code> value
     */
    public void setInitialDir(String initialDir) {
        this.initialDir = initialDir;
    }
    
    /**
     * <code>setCommand</code> set the absolute path to the executable to
     * run. This is required to have a valid request and cannot be empty or null.
     *
     * Example: setCommand("/usr/local/common/blastp");
     *
     * @param command a <code>String</code> that contains full Command to run
     * @exception IllegalAccessException
     *                if set is attempted after submission
     */
    public void setCommand(String command) {
        logger.debug("Set command " + command);
        
        this.command = command;
    }
    
    /**
     * <code>getCommand</code> get the full path of command.
     *
     * @return a <code>String</code> value
     */
    public String getCommand() {
        logger.debug("In getCommand.");
        
        return this.command;
    }
    
    /**
     * The method <code>getRetryCount<code>
     *
     * @return an <code>int</code
     *
     */
    public int getRetryCount() {
        return this.retryCount;
    }
    
    /**
     * The method <code>setRetryCount<code>
     *
     * @param retryCount an int
     *
     */
    public void setRetryCount(int retryCount) {
        this.retryCount = retryCount;
    }
    
    /**
     * @return
     */
    public int getMaxWorkers() {
        return this.maxWorkers;
    }
    
    /**
     * @param pMaxWorkers
     */
    public void setMaxWorkers(int pMaxWorkers) {
        this.maxWorkers = pMaxWorkers;
    }
    
    public int getNumTasks() {
        return this.numTasks;
    }
    
    public void setNumTasks(int pNumTasks) {
        this.numTasks = pNumTasks;
    }
    
    
    public String getQueue() {
        return queue;
    }
    
    public void setQueue(String pQueue) {
        this.queue = pQueue;
    }
    
    
    /**
     * <code>getStatus</code> get CommandStatus Object
     *
     * @return a <code>CommandStatus</code> object
     */
    public CommandStatus getStatus() {
        return status;
    }
    
    /**
     * <code>setStatus</code> set the CommandStatus
     *
     * @param pStatus a <code>CommandStatus</code> value
     */
    public void setStatus(CommandStatus pStatus) {  
        status = pStatus;
        
        status.setCommand(this);
    }
    
    /**
     * <code>getConfig</code> get CommandConfig that contains the meta info about the
     * command
     *
     * @return a <code>CommandConfig</code> value
     */
    public CommandConfig getConfig() {
        return this.config;
    }
    
    /**
     * <code>setConfig</code> set CommandConfig that contains the meta info about the
     * command
     *
     * @param pConfig a <code>CommandConfig</code> value
     */
    public void setConfig(CommandConfig pConfig) {
        logger.debug("In setConfig.");
        
        this.config = pConfig;
        
        config.setCommand(this);
    }
    
    /**
     * <code>addParam</code> add a parameter to the parameter list for this Command.
     *
     * @param pParam a <code>Param</code> value
     */
    public void addParam(Param pParam) {
        logger.debug("method Command.addParam arg:" + pParam.toStringNormal());
        pParam.setCommand(this);
        params.add(pParam);
    }
    
    
    /**
     * <code>addParam</code> add a parameter to the parameter list for this command.
     *
     * @param pParam a <code>Param</code> value
     */
    public void addParams(Param pParam) {
        logger.debug("In addParams: " + pParam);
        pParam.setCommand(this);
        params.add(pParam);
    }
    
    /**
     * <code>addParam</code> add a parameter to the parameter list for this command.
     *
     * @param pValue
     */
    public void addParam(String pValue) {
        logger.debug("In addParam:" + pValue);
        
        Param par = new Param(pValue);
        
        par.setCommand(this);
        params.add(par);
    }
    
    /**
     * <code>addParam</code> add a parameter to the parameter list for this command.
     *
     * @param pValue
     * @param pType
     */
    public void addParam(String pValue, ParamType pType) {
        logger.debug("In addParam: " + pValue);
        
        Param par = new Param(pValue, pType);
        
        par.setCommand(this);
        params.add(par);
    }
    
    /**
     * <code>addParam</code> add a parameter to the parameter list for this command.
     *
     *
     * @param pKey
     * @param pValue
     */
    public void addParam(String pKey, String pValue) {
        
        logger.debug("In addParam:" + pKey + " arg:" + pValue);
        
        Param par = new Param(pKey, pValue);
        
        par.setCommand(this);
        params.add(par);
    }
    
    /**
     * <code>addParam</code> add a parameter to the parameter list for this command.
     *
     *
     * @param key
     * @param value
     * @param type
     */
    public void addParam(String key, String value, ParamType type) {
        
        logger.debug("In addParam arg:" + key + " arg:" + value);
        
        Param par = new Param(key, value, type);
        
        par.setCommand(this);
        params.add(par);
    }


    
    /**
     * The method <code>setParams</code> adds all the parameters passed as a list
     * 
     * @param params
     */
    public void setParams(List params) {
        for (Iterator iter = params.iterator(); iter.hasNext();) {
            addParam((Param) iter.next());
        }
    }
    
    /**
     * <code>getParam</code> parameter at index
     *
     * @param index
     * @return a <code>Param</code> value
     */
    public Param getParam(int index) {
        
        logger.debug("In getParam.");
        
        return (Param) params.get(index);
    }
    
    /**
     * Describe <code>getParams</code> method here.
     *
     * @return a <code>Collection</code> value
     */
    public ArrayList getParams() {
        logger.debug("In getParams.");
        
        return params;
    }
    
    
    /**
     * <code>getParamsCount</code> get the number of parameters for this command.
     *
     * @return an <code>int</code> value
     */
    public int getParamsCount() {
        logger.debug("In getParamsCount.");
        
        return params.size();
    }
    
    /**
     * <code>getError</code> get the file that STDERR is being redirected to.
     *
     * @return a <code>String</code> value
     */
    public String getError() {
        return error;
    }
    
    /**
     * <code>setError</code> setter for a file to send the what the command would
     * usually send to the screen or STDERR. This is needed since the command will be
     * running remotely. This can be an absolute path or relative to InitialDir. If not
     * set all output ignored and lost.
     *
     * @param pError
     * @exception IllegalAccessException
     *                if set is attempted after submission
     */
    public void setError(String pError) {
        this.error = pError;
    }
    
    /**
     * <code>getInput</code> get the file that STDIN is being read from.
     *
     * @return a <code>String</code> value
     */
    public String getInput() {
        return input;
    }
    
    /**
     * <code>setInput</code> setter for a file to read standard input from. This is
     * needed since the command will be run remotely and and input can't be read from the
     * keyboard. If not set input is ignored. This can be an absolute path or relative to
     * InitialDir.
     * 
     * @param pInput
     * @exception IllegalAccessException if set is attempted after submission
     */
    public void setInput(String pInput) {
        this.input = pInput;
    }
    
    /**
     * <code>getOutput</code> get the file that STDOUT is being redirected to.
     *
     * @return a <code>String</code> value
     */
    public String getOutput() {
        return output;
    }
    
    /**
     * <code>setOutput</code> setter for a file to redirect STDOUT to. This is needed
     * since the command will be running remotely. This can be an absolute path or
     * relative to InitialDir. If not set, all output is ignored and lost.
     *
     * @param pOutput a <code>String</code> value
     * @exception IllegalAccessException if set is attempted after submission
     */
    public void setOutput(String pOutput) {
        this.output = pOutput;
    }
    
    /**
     * The method <code>getGridJobID</code> returns the value for gridJobID
     *
     * @return Returns a <code>String</code> value for the field gridJobID.
     */
    public String getGridJobID() {
        return gridJobID;
    }
    
    /**
     * The method <code>setGridJobID</code> sets the value for gridJobID
     *
     * @param gridJobID a <code>String</code> value for the field gridJobID.
     */
    public void setGridJobID(String gridId) {
        if (logger.isDebugEnabled()) {
            logger.debug("Setting grid job id: " + gridId);
        }
        this.gridJobID = gridId;
    }
    
    /**
     * The method <code>getParam</code> returns the parameter with the specified key.
     * 
     * @param key a <code>String</code> representing the parameter name
     * 
     * @return a <code>Param</code> with the specified key, null if one cannot be found
     */
    public Param getParam(String key) {
        for (Iterator iter = params.iterator(); iter.hasNext();) {
            Param param = (Param) iter.next();
            if (param.getKey().equals(key)) {
                return param;
            }
        }
        return null;
    }
    
    /**
     * The method <code>getParamType</code> returns the parameter with the specified parameter type.
     * 
     * @param key a <code>ParamType</code> representing the parameter type
     * 
     * @return a <code>Param</code> of the specified type, null if one cannot be found
     */
    public Param getParam(ParamType pType) {
        for (Iterator iter = params.iterator(); iter.hasNext();) {
            Param param = (Param) iter.next();
            if (param.getType().equals(pType)) {
                return param;
            }
        }
        return null;
    }
    
    public static String getDirArrayParam (String pDirName, int index) {
        String param = "";
        if (logger.isDebugEnabled()) {
            logger.debug("Attempting to read index param from name: " + pDirName);
        }
        
        // The specified file parameter can either be a directory or a file
        // whose starting name is used as a pattern so handle accordingly
        File file = new File(pDirName);
        if (file.isDirectory()) {
            // Read all the files in the directory
            File [] files = file.listFiles();
            param = files[index].getAbsolutePath();
        }
        else {
            // As the name is a filename get the directory component
            File dir = file.getParentFile();
            
            String filePattern = file.getName();
            if (logger.isDebugEnabled()) {
                logger.debug(" directory: " + dir + " filepattern: " + filePattern);
            }
            File [] files = dir.listFiles(new FilenameFilter(pDirName));
            param = files[index].getAbsolutePath();
        }
        
        return param;
    }
    
    /**
     * The method <code>getFileArrayParam</code> returns the parameter at the specified index
     * in the file
     * 
     * @param fileName a <code>String</code> representing the params file name
     * @param index a <code>int</code> representing the index
     * 
     * @return a <code>String</code> representing the parameter name
     * @throws IOException a <code></code>
     */
    public static String getFileArrayParam (String fileName, int index) throws IOException {
        String param = "";
        if (logger.isDebugEnabled()) {
            logger.debug("Attempting to read index param from file: " + fileName);
        }
        
        // Open the file and skip to the line specified by the index and return that
        // line. The array index is 0 based while line number is 1 based so adjust accordingly
        LineNumberReader lnr = new LineNumberReader(new FileReader(fileName));
        while ((param = lnr.readLine()) != null) {
            logger.debug("Param is " + param);
            if (lnr.getLineNumber() == index+1 ) {
                break;
            }
        }
        lnr.close();
        
        return param;
    }
    
    /**
     * The method <code>getArrayParam</code> returns the parameter at the specified index
     * 
     * @param index an <code>int</code> representing the index
     * @return a <code>String</code> representing the array parameter
     * 
     * @throws IllegalStateException
     * @throws IOException 
     */
    public String getParamAtIndex(int index) throws IllegalStateException, IOException {
        String arrayParam = "";
        logger.debug("In getParamAtIndex method");
        
        if (logger.isDebugEnabled()) {
            logger.debug("Attempting to read the array param at index: " + index);
        }
        if (!type.equals(CommandType.MW)) {
            throw new IllegalStateException("Attempt to retrieve array param for non-array job.");
        }
        
        // Read the param for this Command which is of one of the array types
        // See what type of the standard array types is associated with this command
        // and based on that type retrieve the index param
        if (getParam(ParamType.DIR) != null) {
            // If it is a dir type the retrieve index for directory
            arrayParam = getDirArrayParam(getParam(ParamType.DIR).getValue(), index);
        }
        else if (getParam(ParamType.FILE) != null) {
            // If it is a file type the retrieve index for directory
            arrayParam = getFileArrayParam(getParam(ParamType.FILE).getValue(), index);
        } 
        else if (getParam(ParamType.TEMPFILE) != null) {
            // If it is a temp file type then retrieve the index for directory
            arrayParam = getFileArrayParam(getParam(ParamType.TEMPFILE).getValue(), index);
        }
        else {
            // As this is not a supported type throw exception
            throw new IllegalStateException("An unsupported array type encountered.");
        }
        
        if (logger.isDebugEnabled()) {
            logger.debug("Array param: " + arrayParam);
        }
        
        logger.debug("End getParamAtIndex method");
        
        return arrayParam;
    }
    
    // //////////////////////
    // Task Event Methods //
    // //////////////////////
    
    /**
     * The method <code>getTasks<code>
     *
     *
     * @return
     *
     */
    public synchronized Collection getTasks() {
        logger.debug("In getTasks.");
        return tasks.values();
    }
    
    public synchronized void setTasks(Vector tasks) {
        for (Iterator iter = tasks.iterator(); iter.hasNext();) {
            Task task = (Task) iter.next();
            addTask(task);
        }
    }
    
    /**
     * The method <code>setTasks<code>
     *
     * @param p_tasks
     *
     */
    public synchronized void addTask(Task pTask) {
        logger.debug("In addTask.");
        tasks.put(new Integer(pTask.index), pTask);
    }
    
    /**
     * @param index
     * @return
     */
    public synchronized Task getTask(int index) {
        return (Task) tasks.get(new Integer(index));
    }
    
    public synchronized void taskSubmitted(String machine, String pLog) {
        logger.info("In taskSubmitted: Submit for " + id );
        
        Date ldate = new Date();
        
        // Set machine name to empty if it is null.
        if (machine == null) {
            machine = "";
        }
        String message = "Command(" + id + ") was submitted by " + username + " on "
        + machine + " at " + ldate;
        SubmitEvent event = new SubmitEvent(this, id, ldate, message, machine, username, pLog);
        
        logger.info("taskSubmitted : Submit for " + id);

        submitTime = ldate.getTime();
        logger.info("taskSubmitted : Submit time for " + id + " is " +submitTime);
        synchronized (this) {
            status.waiting++;
            state = CmdState.WAITING;
            submitURL = machine;
            status.setMessage(message);
        }
        
        updateCommand();
        
        logger.debug("Changed state to waiting now let all listerners know");
        logger.info("Firing submit event " + event.toString());
        disp.fireSubmitEvent(event);
    }
    
    
    /**
     * The method <code>taskStarted<code>
     *
     * @param message
     * @param date
     * @param machine
     * @param index
     * @param worker
     */
    public synchronized void taskStarted(String message, Date date, String machine, int index,
            int worker) {
        logger.info("Task Event: Start for " + id + " index " + index );
        
        boolean firstStart = false;
        
        // create a new task
        Task t = new Task();
        t.message = message;
        t.worker = worker;
        t.machine = machine;
        t.state = CmdState.RUNNING;
        t.index = index;
        t.cmd = this.id;
        
        tasks.put(new Integer(t.index), t);
        
        TkStartEvent event = new TkStartEvent(this, id, date, message, machine, index);
        
        // update Command start
        synchronized(this) {
            // update status
            status.running++;
            status.waiting--;
            logger.info("Setting State to Running for job " + id + " and index " + index);

            if(state == CmdState.WAITING) {
                state = CmdState.RUNNING;
                status.setMessage(message);
                firstStart = true;
            } else if(state == CmdState.INIT) {
                state = CmdState.RUNNING;
                status.setMessage(message);
                firstStart = true;
                logger.warn(id + " !MISSING SUBMIT EVENT! Had a Start Event for Command in INIT, should have been in WAITING\n");
                logger.warn(" Going to change state to RUNNING anyways");
            }
            startTime = date.getTime();
        }

        // update the Command in the database before sending events
        updateCommand();
        
        if (firstStart) {
            logger.info("Command event: Start for " + id);
            disp.fireStartEvent((StartEvent) event);
        }
        
        disp.fireTkStartEvent(event);
    }
    
    
    /**
     * @param date
     */
    protected void fireLastEvent(Date date) {
        logger.debug("Command state is " + state);
        
        if (state == CmdState.FAILED) {
            String mess = "Command failed sub jobs status of " + status.errors + " failed "
            + status.interrupted + " interrupted " + status.completed + " finished ";
            FailureEvent fevent =  new FailureEvent(this, id, (Date) date.clone(), 
                    mess, new Exception(mess));
            disp.fireFailureEvent(fevent);
        } else if (state == CmdState.INTERRUPTED) {    // fire interrupted
            // TODO: propagate right interrupt type
            String mess = "Command Interrupted " + status.interrupted 
            + " jobs interrupted";
            InterruptEvent ievent =  new InterruptEvent(this, id, (Date) date.clone(), 
                    mess, CommandInterrupt.SYSTEM_KILL);
            disp.fireInterruptEvent(ievent);
        }
    }
    
    /**
     * @param date
     * @param returnValue
     */
    protected void fireLastEvent(Date date, int returnValue) {
        logger.debug("Command state is " + state);
        
        if(state == CmdState.FAILED) {
            String mess = "Command failed sub jobs status of " + status.errors + " failed "
            + status.interrupted + " interrupted " + status.completed + " finished ";
            FailureEvent fevent =  new FailureEvent(this, id, (Date) date.clone(), 
                    mess, new Exception(mess));
            disp.fireFailureEvent(fevent);
        }
        else if (state == CmdState.INTERRUPTED) {    // fire interrupted
            // TODO: propagate the right interrupt type
            String mess = "Command Interrupted " + status.interrupted 
            + " jobs interrupted";
            InterruptEvent ievent =  new InterruptEvent(this, id, (Date) date.clone(), 
                    mess, CommandInterrupt.SYSTEM_KILL);
            disp.fireInterruptEvent(ievent);
        }
        else {
            String mess = "Command and " + status.completed + " sub jobs are finished";
            disp.fireFinishEvent(new FinishEvent(this, id, date, mess, returnValue));
        }
    }
    
    /**
     * The method <code>taskFinished<code>
     *
     * @param message
     * @param date
     * @param returnValue
     * @param index
     */
    public synchronized void taskFinished(String message, Date date, int returnValue, int index) {
        boolean done = false;
        
        logger.info("In taskFinished for " + id + ", index " + index + ", return value " + returnValue);
        
        // update task
        logger.info("Tasks data structure is " + tasks.toString());
        Task t = (Task) tasks.get(new Integer(index));
        
        if (t != null) {
        	logger.debug("Task notification received for Task " + t);
            t.returnValue = returnValue;
            t.state = CmdState.FINISHED;
            t.message = message;
            tasks.put(new Integer(t.index), t);
        } else {
            logger.warn("Task " + index + " finished but it was never started...");
            return;
        }
        
        // TODO : make sure the same task doesn't start multiple times
        // update status variables and state
        synchronized (this) {
            status.running--;
            status.completed++;
            // Set return value to status object
            status.setReturnValue(returnValue);
            int total = status.completed + status.interrupted + status.errors;
            logger.debug("Completed: " + status.completed + "; Interrupted: " + status.interrupted +
            		     "; Errors: " + status.errors);
            if (total >= numTasks) {
                done = true;
                if (status.errors > 0) {
                	state = CmdState.FAILED;
                } else if (status.interrupted > 0) {
                	state = CmdState.INTERRUPTED;
                } else {
                	state = CmdState.FINISHED;
                }
                endTime = date.getTime();
            }
        }
        
        logger.debug("Just before update, the Command state is: " + state);
        // Update the Command in the database before sending events
        updateCommand();
        
        // Create the finish event
        TkFinishEvent event = new TkFinishEvent(this, id, date, message, returnValue, index);
        disp.fireTkFinishEvent(event);
        
        if (numTasks == 0) {
        	// The job was NOT an array job
            logger.info("The event return value is " + ((FinishEvent)event).getReturnValue());
            disp.fireFinishEvent((FinishEvent) event);
        } else {
        	// The job WAS an array job
            if (done) {
                logger.debug("Fire last event");
                fireLastEvent(date, returnValue);
            }
        }
    }
    
    
    /**
     * The method <code>taskFailure<code>
     *
     * @param exception
     * @param index
     *
     */
    public synchronized void taskFailure(Throwable exception, int index) {
        boolean done = false;
        Date ldate = new Date();
        
        logger.info("Task failure for " + id + " index " + index);
        
        // update task
        Task t = (Task) tasks.get(new Integer(index));
        
        if(t != null) {
            t.message = exception.getMessage();
            t.state = CmdState.FAILED;
            tasks.put(new Integer(t.index), t);
        } else {
            logger.warn("A task failed that was never started...");
            state = CmdState.FAILED;
        }
        
        // update status variables and state
        synchronized (this) {
            status.running--;
            status.errors++;
            int total = status.completed + status.interrupted + status.errors;
            
            if (total >= numTasks) {
                done = true;
                if(status.errors > 0) state = CmdState.FAILED;
                else if(status.interrupted > 0)	state = CmdState.INTERRUPTED;
                else state = CmdState.FINISHED;
                endTime = ldate.getTime();
            }
        }
        
        // create failure event
        TkFailureEvent event = new TkFailureEvent(this, id, ldate, exception.getMessage(),
                exception, index);
        
        // update command in database before sending events
        updateCommand();
        
        disp.fireTkFailureEvent(event);

        if (numTasks == 0) {
        	// This is not an array job
            disp.fireFailureEvent((FailureEvent) event);
        } else if(done) {
            fireLastEvent(ldate);
        }
    }
    
    /**
     * The method <code>taskInterrupted<code>
     *
     * @param message
     * @param interrupt
     * @param index
     *
     */
    public synchronized void taskInterrupted(String message, CommandInterrupt interrupt, int index) {
        boolean done = false;
        Date ldate = new Date();
        
        logger.info("In taskInterrupted for " + id + " index " + index);
        
        // update task
        Task t = (Task) tasks.get(new Integer(index));
        
        if(t == null) {
            // create new task
            logger.info("task was interrupted before it was started");
            t = new Task();
            t.cmd = this.id;	
        }
        
        if (t != null) {
            t.message = message;
            tasks.put(new Integer(t.index), t);
        }
        
        // The update status variables and state
        synchronized (this) {
            status.running--;
            status.interrupted++;
            int total = status.completed + status.interrupted + status.errors;
            
            if (total >= numTasks) {
                done = true;
                if (status.errors > 0) {
                	state = CmdState.FAILED;
                } else if (status.interrupted > 0) {
                	state = CmdState.INTERRUPTED;
                } else {
                	state = CmdState.FINISHED;
                }
                endTime = ldate.getTime();
            }
        }
        
        logger.info("Update Command in task interrupt");
        // Update the Command in the database before sending events
        updateCommand();
        
        TkInterruptEvent event = new TkInterruptEvent(this, id, ldate, message, interrupt, index);
        logger.info("Fire the task interrupt");
        disp.fireTkInterruptEvent(event);
        
        if (numTasks == 0) {
            // This is not an array job
        	logger.info("Firing full Command interrupt.");
            disp.fireInterruptEvent((InterruptEvent) event);
        } else if (done) {
            fireLastEvent(ldate);
        }
    }
    
    /**
     * The method <code>printTasks<code>
     *
     * @return
     *
     */
    public synchronized String printTasks() {
        String taskStr = "";
        Iterator i = tasks.values().iterator();
        
        for(int count = 0; i.hasNext(); count++) {
            Task task = (Task) i.next();
            
            taskStr += " Task(" + count + ") " + task.state + " " + task.index + " "  +
            		task.returnValue + " " + task.machine + "\n";
        }
        
        return taskStr;
    }
    
    // 
    // TimeStampable
    // 
    
    /**
     * The method <code>jdoSetTimeStamp<code>
     *
     * @param pTimestamp
     *
     */
    public void jdoSetTimeStamp(long timestamp) {
        logger.debug("Changing timestamp from: " + this.timeStamp + " to: " + timestamp);
        
        this.timeStamp = timestamp;
    }
    
    /**
     * The method <code>jdoGetTimeStamp<code>
     *
     * @return a long
     */
    public long jdoGetTimeStamp() {
        logger.debug("GRABBING TIMESTAMP " + timeStamp);
        
        return timeStamp;
    }
    
    // 
    // Object overrides (clone, toString, etc)
    // 
    
    /**
     * The method <code>setAllFields</code> copies all the fields to the passed copy. If
     * the property is a complex property then a clone is made
     *
     * @param copy a <code>Command</code> to which all the fields from this Command
     * are copied
     *
     * @param copy
     */
    protected void setAllFields(Command copy) {
        
        super.setAllFields(copy);

        if(config != null) copy.config = (CommandConfig) config.clone();
        
        if(status != null) copy.status = (CommandStatus) status.clone();
        
        if(params != null) copy.params = (ArrayList) params.clone();
        
        copy.initialDir = initialDir;
        copy.output = output;
        copy.input = input;
        copy.error = error;
        copy.command = command;
        copy.retryCount = retryCount;
        copy.times = times;
        copy.numTasks = numTasks;
        copy.maxWorkers = maxWorkers;
        copy.gridJobID = gridJobID;
        copy.type = type;
        copy.queue = queue;
        copy.monitorHost = monitorHost;
        
        if (requestTime != 0) {
        	copy.requestTime = requestTime;
        }
    }
    

    /* (non-Javadoc)
     * @see java.lang.Object#clone()
     */
    @Override
    public Object clone() {
        Command copy = new Command();
        
        setAllFields(copy);
        
        return copy;
    }
    
    /**
     * <code>toString</code> returns an XML representation of the Command using the
     * CommandHelper to do the conversion.
     *
     * @return a <code>String</code> value
     */
    @Override
    public String toString() {
        return CommandHelper.getInstance().marshal(this);
    }
    
    /**
     * <code>toString</code> returns an XML representation of the Command using the
     * CommandHelper to do the conversion.
     *
     * @return a <code>String</code> value
     */
    public String toXMLString() {
        return CommandHelper.getInstance().marshal(this);
    }

    /**
     * The constructor <code>Command</code> creates a new instance
     * 
     * @param host
     */
    public Command(String host) {
        this.monitorHost = host;
    }

    /**
     * The method <code>getMonitorHost</code> returns the value for the field monitorHost. 
     *
     * @return a <code>String</code> representing the value for the field <code>monitorHost</code>.
     */
    public String getMonitorHost() {
        return this.monitorHost;
    }

    /**
     * The method <code>setMonitorHost</code> sets the value for the field monitorHost.
     *
     * @param monitorHost The monitorHost to set.
     */
    public void setMonitorHost(String monitorHost) {
        this.monitorHost = monitorHost;
    }

    /**
     * The method <code>getRequestTime</code> returns the time the request was received
     * 
     * @return a <code>Date</code> representing the request time
     */
    public long getRequestTime() {
        return this.requestTime;
    }

    /**
     * The method <code>setRequestTime</code> sets the request time for this command
     * 
     * @param requestTime a <code>Date</code> representing the time the request was received
     */
    public void setRequestTime(long requestTime) {
        this.requestTime = requestTime;
    }

    /**
     * The method <code>isSendEvents</code> returns the value for the field sendEvents. 
     *
     * @return a <code>boolean</code> representing the value for the field <code>sendEvents</code>.
     * A value of true indicates that events need to be sent to client, false otherwise
     */
    public boolean isSendEvents() {
        return this.sendEvents;
    }

    /**
     * The method <code>setSendEvents</code> sets the value for the field sendEvents.
     *
     * @param sendEvents The sendEvents flag to set.
     */
    public void setSendEvents(boolean sendEvents) {
        this.sendEvents = sendEvents;
    }
}
